#
# Copyright (C) 2008 Dag Brattli, ObexCode
#
# Portions copyright 2007 Google Inc. (GData Python Client Library)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from datetime import datetime

# Import ElementTree API. You can override it by setting the ET class member
try:
    from xml.etree import cElementTree as ET
except ImportError:
    try:
        import cElementTree as ET
    except ImportError:
        from elementtree import ET

# In-place ElementTree pretty print formatter
def indent(elem, level=0, spaces="  "):
    i = "\n" + level*spaces
    if len(elem):
        if not elem.text or not elem.text.strip():
            elem.text = i + spaces

        if level and (not elem.tail or not elem.tail.strip()):
            elem.tail = i

        for elem in elem:
            indent(elem, level+1)
        
        if not elem.tail or not elem.tail.strip():
            elem.tail = i
    else:
        if level and (not elem.tail or not elem.tail.strip()):
            elem.tail = i

class Elements(object):
    """Elements is the base class to be used for your XML objects. You will
    need to subclass Elements for all XML elements you want to parse."""
    
    _children = {}
    _attributes = {}
    _namespace = None
    
    ET = ET # So you can override and use a different ElementTree API 

    def __init__(self, text=None):
        self.text = text
        
    def __setattr__(self, name, value):
        """This is a bit of magic. We override setattr in order to record the 
        ordering of the member variables in the __init__ method. This way we 
        can force the same ordering when converting back to ElementTree/XML 
        again"""
        
        if not hasattr(self, '_ordering'):
            object.__setattr__(self, '_ordering', [])
        if name not in self._ordering:
            self._ordering.append(name)
        
        object.__setattr__(self, name, value)
        
    def _convert_element_tree_to_member(self, child_tree):
        if child_tree.tag[0] == '{':
            split = child_tree.tag.split('}')
            ns = split[0][1:]
            tag = split[1]
        else:
            tag = child_tree.tag
            ns = None

        # Find the element's tag in this class's list of children
        if self.__class__._children.has_key(tag) and (ns == self.__class__._namespace):
            child = self.__class__._children[tag]
        elif self.__class__._children.has_key(child_tree.tag):
            child = self.__class__._children[child_tree.tag]
        else:
            print 'convert_element_tree_to_member(): Unknown member %s' % child_tree.tag    
            return
        
        member_name = child[0]
        
        # If no class, then we assume the same class as ourselves. This is to
        # enable Elements classes to be self referencing.
        try:
            member_class = child[1]
        except IndexError:
            member_class = self.__class__
        
        # If the class member is supposed to contain a list, make sure the
        # matching member is set to a list, then append the new member
        # instance to the list.
        #print 'member_class: ', member_class
        if type(member_class) == type and issubclass(member_class, basestring):
            if issubclass(member_class, unicode):
                setattr(self, member_name, child_tree.text)
            else:
                setattr(self, member_name, child_tree.text)
        elif type(member_class) == type and issubclass(member_class, bool):
            setattr(self, member_name, True)
        elif type(member_class) == type and issubclass(member_class, int):
            setattr(self, member_name, int(child_tree.text))
        elif type(member_class) == type and issubclass(member_class, datetime):
            import dateutil.parser
            setattr(self, member_name, dateutil.parser.parse(child_tree.text))
        elif isinstance(member_class, list):
            # Make sure member is a list
            if getattr(self, member_name) is None:
                setattr(self, member_name, [])

            # If member class is missing, the we assume the same class as 
            # we are. This is a feature to enable self referencing Elements
            try:
                new_class = member_class[0]
            except IndexError:
                new_class = self.__class__

            if issubclass(new_class, Elements):
                new_member = new_class()
                new_member.from_element_tree(tree=child_tree)
                getattr(self, member_name).append(new_member)
            elif issubclass(new_class, unicode):
                getattr(self, member_name).append(child_tree.text)
            elif issubclass(new_class, int):
                getattr(self, member_name).append(int(child_tree.text))
            else:
                print 'convert_element_attribute_to_member(): Unknown member class %s' % repr(member_class[0])
            
        elif isinstance(member_class, dict):
            key = member_class.keys()[0]
            new_member = member_class[key]()
            new_member.from_element_tree(child_tree)
            
            if key[0] == '{':
                index = child_tree.findtext('%s' % key)
            else:
                index = child_tree.findtext('{%s}%s' % (new_member.__class__._namespace, key))
            getattr(self, member_name)[index] = new_member
        else:
            new_member = member_class()
            new_member.from_element_tree(child_tree)
            setattr(self, member_name, new_member)

    def _convert_element_attribute_to_member(self, attribute, value):
        # Find the attribute in this class's list of attributes. 
        if not self.__class__._attributes.has_key(attribute):
            print 'convert_element_attribute_to_member(): Unknown attribute %s' % attribute
            return
        # Find the member of this class which corresponds to the XML attribute
        # (lookup in current_class._attributes) and set this member to the
        # desired value (using self.__dict__).
        if value:
            # Encode the string to capture non-ascii characters (default UTF-8)
            setattr(self, self.__class__._attributes[attribute], value)

    # Three methods to create an object from an ET
    def _harvest_element_tree(self, tree):
        # Fill in the instance members from the contents of the XML tree.

        for child in tree:
            self._convert_element_tree_to_member(child)
        for attribute, value in tree.attrib.iteritems():
            self._convert_element_attribute_to_member(attribute, value)

        self.text = tree.text

    # Helper function to add string children to an ET
    def _add_string_to_element_tree(self, tag, member):
        new_child = self.ET.Element(tag)
        new_child.text = member
        return new_child

    # Three methods to create an ET from an object
    def _add_members_to_element_tree(self, tree):
        # Convert the members of this class which are XML child nodes. 
        # This uses the class's _children dictionary to find the members which
        # should become XML child nodes.
        
        children = self.__class__._children.items()
        
        # Sort children according to ordering listed in __init__() method
        children.sort(cmp=lambda x,y: cmp(self._ordering.index(x[1][0]), self._ordering.index(y[1][0])))

        for tag, value in children:
            member_name = value[0]
            member = getattr(self, member_name)
            
            # Skip members with None as value
            if member is None:
                pass
                
            elif isinstance(member, basestring):
                new_child = self._add_string_to_element_tree(tag, member)
                tree.append(new_child)
            elif isinstance(member, bool):
                # Only include bool elements that are True
                if member:
                    new_child = self.ET.Element(tag)
                    tree.append(new_child)
            elif isinstance(member, int):
                new_child = self._add_string_to_element_tree(tag, str(member))
                tree.append(new_child)              
            elif isinstance(member, list):
                for instance in member:
                    if isinstance(instance, basestring): # List of strings
                        new_child = self._add_string_to_element_tree(tag, instance)
                        tree.append(new_child)
                    elif isinstance(instance, int): # List of integers
                        new_child = self._add_string_to_element_tree(tag, str(instance))
                        tree.append(new_child)
                    else: # List of Elements subclass
                        instance._become_child_element(tree)
                        
            elif isinstance(member, dict):
                for instance in member.values():
                    instance._become_child_element(tree)
            elif isinstance(member, datetime):
                new_child = self._add_string_to_element_tree(tag, member.strftime(value[2]))
                tree.append(new_child)
            else:
                member._become_child_element(tree)
                
        # Convert the members of this class which are XML attributes.
        for xml_attribute, member_name in self.__class__._attributes.iteritems():
            member = getattr(self, member_name)
            if member is not None:
                tree.attrib[xml_attribute] = member
    
        if hasattr(self, 'text'):
            tree.text = self.text
    
    def _become_child_element(self, tree):
        """
        Note: Only for use with classes that have a _tag and _namespace class 
        member. It is in Elements so that it can be inherited but it should
        not be called on instances of Elements.
        """
        if self.__class__._namespace:
            nsmap = { None : self.__class__._namespace }
            tag = "{%s}%s" % (self.__class__._namespace, self.__class__._tag)
        else:
            tag = self.__class__._tag
            nsmap = None

        new_child = self.ET.Element(tag) #, nsmap=nsmap)
        tree.append(new_child)
        self._add_members_to_element_tree(new_child)

    def to_element_tree(self):
        """Convert this Elements object to an ElementTree"""
        if self.__class__._namespace:
            tag = "{%s}%s" % (self.__class__._namespace, self.__class__._tag)
            nsmap = { None : self.__class__._namespace }
        else:
            tag = self.__class__._tag
            nsmap = None
        
        new_tree = self.ET.Element(tag) #, nsmap=nsmap)
        self._add_members_to_element_tree(new_tree)
        return new_tree

    def from_element_tree(self, tree):
        """Parse ElementTree into this Elements object"""
        if self.__class__._namespace:
            match = "{%s}%s" % (self._namespace, self._tag)
        else:
            match = self._tag
        
        #print tree.tag, match
        if tree.tag == match:
            self._harvest_element_tree(tree)
            ret = True
        else:
            print 'from_element_tree(): Unknown element tag %s' % tree.tag
            ret = False
        return ret

    def to_string(self, pretty_print=True):
        """Convert this Elements object to XML string"""
        etree = self.to_element_tree()
        if pretty_print:
            indent(etree)
        return self.ET.tostring(etree)

    def from_string(self, xml_string):
        """Parse XML string into this Elements object"""
        tree = self.ET.fromstring(xml_string)
        return self.from_element_tree(tree)
        
    def from_file(self, filename):
        """Parse XML file into this Elements object"""
        f = open(filename)
        ret = self.from_string(f.read())
        f.close()
        return ret
        
    def __str__(self):
        return self.to_string()

